今天要來開發主要功能了!
進度圖沒有變化。
昨天介紹了專案架構與主程式,今天要來繼續開發主要功能。不過礙於版面有限,今天只會介紹到線索管理功能,其他剩餘部分就留到明天吧!
複習一下使用者故事。
- 作為頻道成員,我可以輸入線索,讓 Discord BOT 取得並儲存相關資訊。
- 作為頻道成員,若我輸入的線索格式不正確,會收到錯誤提示,且該線索不會被記錄。
- 線索只能在特定頻道中提交。
這個應該是數一數二重要的功能了。
照理來說,應該要使用指令來觸發報線索相關功能 (不論是最基本的 command 或是 slash command),但由於大家已經習慣只在某個特定頻道報線索了 (這是最一開始的作法),所以希望可以維持不變,直接輸入線索就好。
因此,報線索比較適合透過監聽 on_message
事件來觸發。
@bot.event
async def on_message(message: discord.Message):
if message.author == bot.user:
return
# do something here
接下來,還要驗證是否是在「報線索頻道」、線索格式是否正確,都符合才會更新線索資訊。如果不符合,立刻在下方留言告知格式不正確。不過,可惜的是,這不是互動性的指令,無法使用 empheral
屬性,大家都會知道有人輸入的線索格式錯誤了。
判斷「是否為可以提交線索的特定成員」的部分在
update_clue
內才進行。
@bot.event
async def on_message(message: discord.Message):
if message.author == bot.user:
return
if message.channel.id != CLUE_CHANNEL_ID:
return
if validate_clue(message.content):
update_clue(message.author.id, message.content)
else:
await message.channel.send("線索格式不正確!")
在錯誤提示的地方還有一些地方可以微調,例如:改成用回覆的方式,讓大家可以比較清楚知道是哪一則留言格式錯誤,避免同時有兩人報線索,卻無法判斷到底是哪則留言格式不對。
await message.reply("線索格式不正確!")
或者,覺得這樣公開讓大家知道會讓留言的人很尷尬,改成讓這個錯誤訊息只出現 10 秒鐘,時間到就自動刪除。
response = await message.reply("線索格式不正確!")
await response.delete(delay=10)
最後,一些重複性的判斷可以獨立出來變成一個函數,或甚至改成 Decorator 也可以。
class ClueCog(commands.Cog):
def __init__(self, bot: commands.Bot):
self.bot = bot
def is_valid_message(self, message: discord.Message) -> bool:
if message.author == self.bot.user:
return False
if message.channel.id != settings.clue_channel_id:
return False
return True
@commands.Cog.listener()
async def on_message(self, message: discord.Message):
if not self.is_valid_message(message):
return
if validate_clue(message.content):
update_clue(message.author.id, message.content)
else:
response = await message.reply("線索格式不正確!")
await response.delete(delay=10)
- 作為頻道成員,我可以編輯線索,並且 Discord BOT 會更新該線索資訊並檢查其格式。
- 作為頻道成員,我可以刪除線索,Discord BOT 會相應更新線索資訊。
這兩個需求就比較直觀了,使用 on_message_edit
與 on_message_delete
就可以。其他檢查的部分就沿用前面「設定線索」的需求。
@commands.Cog.listener()
async def on_message_edit(self, before: discord.Message, after: discord.Message):
if not self.is_valid_message(after):
return
if validate_clue(after.content):
update_clue(after.author.id, after.content)
else:
response = await after.reply("線索格式不正確!")
await response.delete(delay=10)
@commands.Cog.listener()
async def on_message_delete(self, message: discord.Message):
if not self.is_valid_message(message):
return
update_clue(message.author.id, "")
補充說明:編輯訊息不適合使用 message command,因為 message command 不能再額外提供參數。
- 作為頻道成員,我可以下達指令,要求查看目前儲存的線索資訊。
有的時候,會想要確認一下 Discord BOT 到底有沒有成功紀錄到線索 (在早期開發時格外需要),因此就有設計這項功能。另外,也希望這項功能不要影響到原本報線索的畫面,所以會加上 empheral=True
的設定。
@app_commands.command()
async def clues(self, interaction: discord.Interaction):
"""取得所有人的線索資訊"""
clues = get_clues()
await interaction.response.send_message(clues)
- 作為頻道成員,我可以為其他頻道成員設定線索。
在早期開發時,錯誤處理做的還不是很完善,導致有時候會出現「明明有在頻道報線索,雀沒有被正確儲存」的狀況。因此,特別設計一個可以幫忙別人報線索的功能,讓發生上述情況時,其他人可以幫忙設定線索。現在,功能完善很多了,這個功能就幾乎不會用到了,但為了保險起見,還是把這個功能保留下來。
幫別人設定線索時,需要使用者自行帶入兩個參數,一個是要設定的對象,另一個是線索。因此,這邊最適合的做法是使用 slash command。
@app_commands.command()
async def clue(
self, interaction: discord.Interaction, member: discord.Member, clue: str
):
"""更新某人的線索資訊"""
if validate_clue(clue):
update_clue(member.id, clue)
await interaction.response.send_message(
f"{member} 線索已更新!", ephemeral=True
)
else:
await interaction.response.send_message("線索格式不正確!", ephemeral=True)
有一個很重要的點,一定要記得加上 type hint,這樣就可以讓 discord.py 自行幫忙轉換成頻道成員。而且,如此一來,使用者在輸入參數的時候,就會自動出現選項,非常方便。
- 作為頻道成員,我可以下達指令,要求根據歷史訊息來更新已儲存的線索資訊。
跟上面的狀況有點類似,早期開發時,還不太熟悉部屬的部分,所以 Discord BOT 只是單純地在我的筆電上執行。這樣造成的問題是,一旦我的筆電休眠了,Discord BOT 也會跟著下線,進而導致後面的其他成員即使報線索,也無法更新線索資訊。雖然可以使用上面的方法,一個一個慢慢加,但一直複製貼上也是頗麻煩的,因此就設計了一個「使用歷史訊息更新線索」的功能。有了這項功能,即使 Discord BOT 曾經下線一段時間導致線索沒有更新到,也可以很快速地把線索都更新完。這個功能現在也不常用了,同樣也只是為了保險起見才留下來。
如果想要讀取頻道的歷史訊息,可以使用 channel.history
。
counter = 0
async for message in channel.history(limit=200):
if message.author == client.user:
counter += 1
或是
messages = [message async for message in channel.history(limit=123)]
其他參數可以參考文件。
需要注意的是,有些時候,有些人報完線索發現有打錯,想要進行修改,就會再報一次 (那時沒有監聽編輯事件的功能)。而使用上面的方法取得到的歷史訊息是由最新的訊息開始的,這樣會導致使用歷史訊息更新時,舊的線索反而會蓋掉後來修正的線索 (新留言的)。因此,在開始更新之前,要記得把順序反過來。
@app_commands.command()
async def update(self, interaction: discord.Interaction):
"""用歷史訊息更新所有人的線索資訊"""
channel = self.bot.get_channel(settings.clue_channel_id)
messages = [message async for message in channel.history(limit=10)]
reversed_messages = messages[::-1]
for message in reversed_messages:
if message.author == self.bot.user:
continue
if validate_clue(message.content):
update_clue(message.author.id, message.content)
await interaction.response.send_message("線索已更新!", ephemeral=True)
今天介紹了有關線索的功能的實作,剩下的部分 (計算與定時提醒) 明天會繼續介紹。